package git.soulbgm.utils;

import git.soulbgm.pojo.dto.AliveInfo;
import git.soulbgm.pojo.dto.CmdResult;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.stream.Collectors;

/**
 * 执行系统命令工具类
 *
 * @author SoulBGM
 * @date 2020/6/29 15:57
 */
public class CmdUtil {

    private static final DateTimeFormatter DTF_YMDHMSS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
    private static final DateTimeFormatter DTF_YMD = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter DTF_HMSS = DateTimeFormatter.ofPattern("HH:mm:ss.SS");

    public static final String LOCAL_OS;
    public static final String OS_WIN = "Win";
    public static final String OS_LINUX = "linux";

    static {
        LOCAL_OS = System.getProperty("os.name");
    }

    /**
     * 更新系统日期时间
     *
     * @param updateTime 更新时间
     * @return {@link CmdResult}
     */
    public static CmdResult updateSystemDateTime(long updateTime) {
        // ZoneOffset.ofHours(8)   北京时间
        // ZoneOffset.UTC          UTC时间
        // ZoneId.systemDefault()  系统默认时区
        LocalDateTime localDateTime = Instant.ofEpochMilli(updateTime).atZone(ZoneId.systemDefault()).toLocalDateTime();
        if (LOCAL_OS.contains(OS_WIN)) {
            // date yyyy-MM-dd
            // time HH:mm:ss.SS
            String date = localDateTime.format(DTF_YMD);
            String time = localDateTime.format(DTF_HMSS);
            CmdResult execDateResult = exec("date " + date);
            if (!execDateResult.isSuccess()) {
                return execDateResult;
            }
            CmdResult execTimeResult = exec("time " + time);
            if (!execTimeResult.isSuccess()) {
                return execTimeResult;
            }
            return execDateResult;
        } else {
            // date -s "yyyy-MM-dd HH:mm:ss.SSS"
            String date = localDateTime.format(DTF_YMDHMSS);
            return exec("date -s \"" + date + "\"");
        }
    }

    /**
     * 通过进程名称获取进程是否活着
     *
     * @param processName 进程名称
     * @return 活着返回true否则false
     */
    public static AliveInfo isAlive(String processName) {
        String command = "ps -eo lstart,pid,cmd|grep " + processName + " |grep -vE 'bash -c ps|grep --color=auto|grep " + processName + "'";
        if (LOCAL_OS.contains(OS_WIN)) {
            // wmic process where Name="mysqld.exe" get Caption, CreationDate, ProcessId
            command = "wmic process where Name=\"" + processName + "\" get Caption, CreationDate, ProcessId";
        }
        String execResult = execStr(command);
        if (execResult == null) {
            return AliveInfo.down(processName);
        }
        String[] split = execResult.split("\n");
        for (String s : split) {
            if (LOCAL_OS.contains(OS_WIN)) {
                if (s.contains(processName)) {
                    String[] sp = s.split("\\s+");
                    String date = sp[1];
                    return AliveInfo.running(processName, OS_WIN, date.substring(0, date.indexOf(".")), sp[2]);
                }
            } else {
                String curl = "curl http://";
                if (!s.contains(curl) && s.contains(processName)) {
                    String[] sp = s.split("\\s+");
                    return AliveInfo.running(processName, OS_LINUX, Arrays.stream(sp).limit(5).collect(Collectors.joining(" ")), sp[5]);
                }
            }
        }
        return AliveInfo.down(processName);
    }

    /**
     * 通过端口获取进程是否活着
     *
     * @param port 端口
     * @return 活着返回true否则false
     */
    public static boolean isAlive(Integer port) {
        String command = "netstat -apn|grep " + port;
        if (LOCAL_OS.contains(OS_WIN)) {
            command = "netstat -ano|findstr " + port;
        }
        String execResult = execStr(command);
        if (execResult == null) {
            return false;
        }
        String[] split = execResult.split("\n");
        boolean result = false;
        for (String s : split) {
            if (LOCAL_OS.contains(OS_WIN)) {
                result = s.contains("LISTENING") && s.contains(":" + port);
            } else {
                result = s.contains("LISTEN") && s.contains(":" + port);
            }
            if (result) {
                break;
            }
        }
        return result;
    }

    /**
     * Windows ping失败出现的消息
     */
    private final static String WIN_PING_FAILURE_MESSAGE = "数据包: 已发送 = 4，已接收 = 0，丢失 = 4 (100% 丢失)";
    /**
     * Windows ping成功出现的消息
     */
    private final static String WIN_PING_SUCCESS_AND_NO_LOSS_PACKET_MESSAGE = "数据包: 已发送 = 4，已接收 = 4，丢失 = 0 (0% 丢失)";
    /**
     * Linux ping失败出现的消息
     */
    private final static String LINUX_PING_FAILURE_MESSAGE = "4 packets transmitted, 0 received";
    /**
     * Linux ping成功出现的消息
     */
    private final static String LINUX_PING_SUCCESS_AND_NO_LOSS_PACKET_MESSAGE = "4 packets transmitted, 4 received";
    /**
     * ping失败
     */
    public final static int PING_STATUS_FAILURE = 0;
    /**
     * ping成功并且无丢包
     */
    public final static int PING_STATUS_SUCCESS_AND_NO_LOSS_PACKET = 1;
    /**
     * ping成功但有丢包
     */
    public final static int PING_STATUS_SUCCESS_AND_HAVE_LOSS_PACKET = 2;

    /**
     * 测试连通
     *
     * @param ip IP地址
     * @return int 0 ping失败 1 ping成功并且无丢包 2 ping成功但有丢包
     */
    public static int ping(String ip) {
        if (ip == null || ip.trim().isEmpty()) {
            return PING_STATUS_FAILURE;
        }
        /*
        Linux
        ping -c 次数 -s ping包大小 -W 超时时间(秒) IP地址
        Windows
        ping -n 次数 -l ping包大小 -w 超时时间(毫秒) IP地址

        Windows 有此下面句子的代表是ping不通的
        数据包: 已发送 = 4，已接收 = 0，丢失 = 4 (100% 丢失)

        Linux 有此下面句子的代表是ping不通的
        4 packets transmitted, 0 received, +4 errors, 100% packet loss
        4 packets transmitted, 0 received, 100% packet loss, time 3000ms

        Windows 有此下面句子的代表是网络状态优秀
        数据包: 已发送 = 4，已接收 = 4，丢失 = 0 (0% 丢失)

        Linux 有此下面句子的代表是网络状态优秀
        4 packets transmitted, 4 received, 0% packet loss
        4 packets transmitted, 4 received, 0% packet loss, time 2998ms

        除上面情况则代表网络状态不佳有丢包
         */
        String command = "ping -c 4 -W 2 " + ip;
        if (LOCAL_OS.contains(OS_WIN)) {
            command = "ping -n 4 -w 2000 " + ip;
        }
        CmdResult result = exec(command);
        if (!result.isSuccess()) {
            return PING_STATUS_FAILURE;
        }
        String output = result.getOutput();
        if (!LOCAL_OS.contains(OS_WIN)) {
            if (output.contains(LINUX_PING_FAILURE_MESSAGE)) {
                return PING_STATUS_FAILURE;
            } else if (output.contains(LINUX_PING_SUCCESS_AND_NO_LOSS_PACKET_MESSAGE)) {
                return PING_STATUS_SUCCESS_AND_NO_LOSS_PACKET;
            } else {
                return PING_STATUS_SUCCESS_AND_HAVE_LOSS_PACKET;
            }
        } else {
            if (output.contains(WIN_PING_FAILURE_MESSAGE)) {
                return PING_STATUS_FAILURE;
            } else if (output.contains(WIN_PING_SUCCESS_AND_NO_LOSS_PACKET_MESSAGE)) {
                return PING_STATUS_SUCCESS_AND_NO_LOSS_PACKET;
            } else {
                return PING_STATUS_SUCCESS_AND_HAVE_LOSS_PACKET;
            }
        }
    }

    /**
     * 执行命令
     *
     * @param command 命令
     * @return {@link CmdResult}
     */
    public static CmdResult exec(String command) {
        Process p;
        try {
            if (LOCAL_OS.contains(OS_WIN)) {
                p = new ProcessBuilder("cmd", "/c", command).start();
            } else {
                p = new ProcessBuilder("/bin/bash", "-c", command).start();
            }
        } catch (IOException e) {
            return CmdResult.error();
        }

        try (
                InputStream inputStream = p.getInputStream();
                InputStream errorStream = p.getErrorStream()
        ) {
            String output = getStreamOutput(inputStream);
            String errorOutput = getStreamOutput(errorStream);
            if (!errorOutput.isEmpty()) {
                return CmdResult.error(errorOutput);
            } else {
                return CmdResult.success(output);
            }
        } catch (IOException e) {
            return CmdResult.error();
        }
    }

    /**
     * 执行命令 如果执行错误也会将错误信息输出
     *
     * @param command 命令
     * @return 执行命令后返回的内容
     */
    public static String execStr(String command) {
        Process p;
        try {
            if (LOCAL_OS.contains(OS_WIN)) {
                p = new ProcessBuilder("cmd", "/c", command).redirectErrorStream(true).start();
            } else {
                p = new ProcessBuilder("/bin/bash", "-c", command).redirectErrorStream(true).start();
            }
        } catch (IOException e) {
            return null;
        }

        try (
                InputStream inputStream = p.getInputStream()
        ) {
            return getStreamOutput(inputStream);
        } catch (IOException e) {
            return null;
        }
    }

    /**
     * 得到流输出
     *
     * @param is 输入流
     * @return {@link String}
     * @throws IOException IO异常
     */
    private static String getStreamOutput(InputStream is) throws IOException {
        return getStreamOutput(is, LOCAL_OS.contains(OS_WIN) ? "GBK" : "UTF-8");
    }

    /**
     * 得到流输出
     *
     * @param is          输入流
     * @param charsetName 字符集名称
     * @return {@link String}
     * @throws IOException IO异常
     */
    private static String getStreamOutput(InputStream is, String charsetName) throws IOException {
        try (
                InputStreamReader in = new InputStreamReader(is, charsetName);
                BufferedReader br = new BufferedReader(in)
        ) {
            String line, output;
            StringBuilder sbLines = new StringBuilder();
            while ((line = br.readLine()) != null) {
                sbLines.append(line).append("\n");
            }
            output = sbLines.toString();
            return output;
        }
    }

}
